// 15 面向对象程序设计
/**
 * 面向对象程序设计基于三个基本概念：数据抽象、继承和动态绑定。第7章已经介绍了数据抽象的知识，本章将介绍继承和动态绑定。
 * 继承和动态绑定对程序的编写有两方面的影响：一是我们可以更容易地定义与其他类相似但不完全相同的新类；二是在使用这些彼此相似的类编写程序时，我们可以在一定程度上忽略掉它们的区别。
 * 在很多程序中都存在着一些相互关联但是有细微差别的概念。
 * 例如，书店中不同书籍的定价策略可能不同：有的书籍按原价销售，有的则打折销售。有时，我们给那些购买书籍超过一定数量的顾客打折；另一些时候，则只对前多少本销售的书籍打折，之后就调回原价，等等。
 * 面向对象的程序设计（OOP）适用于这类应用。
 */

#include <iterator>
#include <vector>
#include <list>
#include <deque>
#include <forward_list>
#include <string>
#include <array>
#include <stack>
#include <queue>
#include <algorithm>
#include <numeric>
using std::swap;
using std::vector, std::list, std::deque, std::forward_list, std::string, std::array, std::stack, std::queue;
#include "../Chapter07/Sales_data.h"
#include <iostream>
using std::begin, std::cbegin, std::end, std::cend, std::find, std::accumulate, std::equal, std::fill, std::fill_n, std::back_inserter;
using std::cin, std::cout, std::endl;
using std::copy, std::replace, std::replace_copy;
#include <map>
#include <set>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <memory>
#include <new>
using namespace std;
#include "../Chapter13/13.5.cc" // 不能编译，因为重复定义的main函数
#include "../Chapter12/12.1.6.cc" // 不能编译，因为重复定义的main函数
#include <functional>

class Quote
{
public:
    // 如果我们删除的是一个指向派生类对象的基类指针，则需要虚析构函数
    virtual ~Quote() = default; // 动态绑定析构函数
};

class B
{
public:
    B();
    B(const B&) = delete; // 删除的拷贝构造函数
    // 其他成员，不含有移动构造函数
};
class D : public B
{
    // 没有声明任何构造函数
};

class Quote1
{
public:
    Quote1() = default; // 默认构造函数，对成员依次进行默认初始化
    Quote1(const Quote1&) = default; // 默认的拷贝构造函数，对成员依次拷贝
    Quote1(Quote1&&) = default; // 默认的移动构造函数，对成员依次拷贝
    Quote1& operator=(const Quote1&) = default; // 拷贝赋值
    Quote1& operator=(Quote1&&) = default; // 移动赋值
    virtual ~Quote1() = default; // 虚析构函数
};

class Base{
/*...*/
};
class D1 : public Base
{
public:
    // 默认情况下，基类的默认构造函数初始化对象的基类部分
    // 要想使用拷贝或移动构造函数，我们必须在构造函数初始值列表显示地调用该构造函数
    D1(const D1& d1) : Base(d1) // 拷贝基类成员，Base(d1)一般会匹配Base的拷贝构造函数
        /* D1的成员的初始化 */ { /*...*/ }
    D1(D1&& d1) : Base(std::move(d1)) // 移动基类成员
        /* D1的成员的初始化 */ { /*...*/ }

    // Base::operator=(const Base&); // 不会被自动调用
    D1& D1::operator=(const D1& rhs) { // 拷贝赋值
        Base::operator=(rhs); // 调用Base的拷贝赋值，为基类部分赋值
        // 按照过去的方式为派生类的成员赋值
        // 酌情处理自赋值及释放已有资源等情况
        return *this;
    }

    // Base::~Base(); // 被自动调用执行
    ~D1() { /* 该处由用户定义清除派生类成员的操作 */ }
};

class Disc_quote {
};
class Bulk_quote: public Disc_quote {
public:
    using Disc_quote::Disc_quote; // 继承Disc_quote的构造函数
};

int main()
{
    // 15.7 构造函数与拷贝控制
    // 和其他类一样，位于继承体系中的类也需要控制当其对象执行一系列操作时发生什么样的行为，这些操作包括创建、拷贝、移动、赋值和销毁。
    // 如果一个类（基类或派生类）没有定义拷贝控制操作，则编译器将为它合成一个版本。当然，这个合成的版本也可以定义成被删除的函数。

    // 15.7.1　虚析构函数
    // 继承关系对基类拷贝控制最直接的影响是基类通常应该定义一个虚析构函数（参见15.2.1节，第528页），这样我们就能动态分配继承体系中的对象了。
    // 如果我们delete一个Quote＊类型的指针，则该指针有可能实际指向了一个Bulk_quote类型的对象。如果这样的话，编译器就必须清楚它应该执行的是Bulk_quote的析构函数。
    // 通过在基类中将析构函数定义成虚函数以确保执行正确的析构函数版本：
    // 和其他虚函数一样，析构函数的虚属性也会被继承。因此，无论Quote的派生类使用合成的析构函数还是定义自己的析构函数，都将是虚析构函数。
    // 只要基类的析构函数是虚函数，就能确保当我们delete基类指针时将运行正确的析构函数版本：
    // 之前我们曾介绍过一条经验准则，即如果一个类需要析构函数，那么它也同样需要拷贝和赋值操作（参见13.1.4节，第447页）。但基类的析构函数并不遵循上述准则。

    // 虚析构函数将阻止合成移动操作
    // 虚析构函数将阻止合成移动操作基类需要一个虚析构函数这一事实还会对基类和派生类的定义产生另外一个间接的影响：
    // 如果一个类定义了析构函数，即使它通过=default的形式使用了合成的版本，编译器也不会为这个类合成移动操作（参见13.6.2节，第475页）。

    // 15.7.2 合成拷贝控制与继承
    // 基类或派生类的合成拷贝控制成员的行为与其他合成的构造函数、赋值运算符或析构函数类似：它们对类本身的成员依次进行初始化、赋值或销毁的操作。
    //   此外，这些合成的成员还负责使用直接基类中对应的操作对一个对象的直接基类部分进行初始化、赋值或销毁的操作。
    //   例如，· 合成的Bulk_quote默认构造函数运行Disc_quote的默认构造函数，后者又运行Quote的默认构造函数。
    // 对于派生类的析构函数来说，它除了销毁派生类自己的成员外，还负责销毁派生类的直接基类；该直接基类又销毁它自己的直接基类，以此类推直至继承链的顶端。
    // 如前所述，Quote因为定义了析构函数而不能拥有合成的移动操作(除非显示定义一个移动操作)，因此当我们移动Quote对象时实际使用的是合成的拷贝操作（参见13.6.2节，第477页）。

    // 派生类中删除的拷贝控制与基类的关系
    // 某些定义基类的方式也可能导致有的派生类成员成为被删除的函数：
    //   1. · 如果基类中的默认构造函数、拷贝构造函数、拷贝赋值运算符或析构函数是被删除的函数或者不可访问（参见15.5节，第543页），则派生类中对应的成员将是被删除的，
    //        原因是编译器不能使用基类成员来执行派生类对象基类部分的构造、赋值或销毁操作。
    //   2. · 如果在基类中有一个不可访问或删除掉的析构函数，则派生类中合成的默认和拷贝构造函数将是被删除的，因为编译器无法销毁派生类对象的基类部分。
    //   3. · 和过去一样，编译器将不会合成一个删除掉的移动操作。当我们使用=default请求一个移动操作时，如果基类中的对应操作是删除的或不可访问的，那么派生类中该函数将是被删除的，
    //        原因是派生类对象的基类部分不可移动。同样，如果基类的析构函数是删除的或不可访问的，则派生类的移动构造函数也将是被删除的。
    D d; // 正确，D的合成默认构造函数使用B的默认构造函数
    //D d2(d); // 错误，D的合成拷贝构造函数使用B的拷贝构造函数，而B的拷贝构造函数是被删除的，所以D的合成拷贝构造函数也是被删除的
    //D d3(std::move(d)); // 错误，隐式地使用D的移动构造函数，而D的移动构造函数是被删除的
    // 在实际编程过程中，如果在基类中没有默认、拷贝或移动构造函数，则一般情况下派生类也不会定义相应的操作。

    // 移动操作与继承
    // 大多数基类都会定义一个虚析构函数。因此在默认情况下，基类通常不含有合成的移动操作，而且在它的派生类中也没有合成的移动操作。
    // cgs:一个类之所以定义析构函数，一定是涉及了资源管理等操作。涉及资源管理的类，一般也需要定义拷贝操作，而定义了拷贝操作，编译器则不会为其定义合成的移动操作。
    /** 
     * 一个类之所以定义析构函数，确实可能是因为它涉及了资源管理等操作，例如动态内存分配、文件打开/关闭等。在这样的情况下，当对象生命周期结束时，需要释放或清理这些资源。
     * 然而，并非所有涉及资源管理的类都需要定义拷贝构造函数和赋值运算符。如果类的对象可以直接通过浅拷贝来复制（即不涉及资源的所有权转移），那么默认的拷贝构造函数和赋值运算符就可以满足需求。
     * 此外，即使一个类定义了自己的拷贝构造函数和赋值运算符，编译器仍然可以为其合成移动构造函数和移动赋值运算符。但是，由于自定义的拷贝操作可能包含了特定于类的设计决策和行为，
     *   而默认的移动操作可能会与这些设计决策不兼容，因此编译器不会自动进行这项工作。
     * 为了确保代码的行为符合预期，对于涉及资源管理的类，通常建议显式地声明并实现所有的特殊成员函数，包括拷贝构造函数、赋值运算符、移动构造函数和移动赋值运算符。
     *   这样可以帮助避免潜在的问题和错误，并给予程序员对类行为的完全控制权。
    */ 
    // 因为基类缺少移动操作时会阻止派生类拥有自己的合成移动操作，所以当我们确实需要执行移动操作时应该首先在基类中进行定义。
    // 一旦Quote定义了自己的移动操作，那么它必须同时显式地定义拷贝操作（参见13.6.2节，第476页）：见Quote1。

    // 15.7.3 派生类的拷贝控制成员
    // 派生类构造函数在其初始化阶段中不但要初始化派生类自己的成员，还负责初始化派生类对象的基类部分。因此，派生类的拷贝和移动构造函数在拷贝和移动自有成员的同时，也要拷贝和移动基类部分的成员。
    // 和构造函数及赋值运算符不同的是，析构函数只负责销毁派生类自己分配的资源。如前所述，对象的成员是被隐式销毁的（参见13.1.3节，第445页）；类似的，派生类对象的基类部分也是自动销毁的。
    // 当派生类定义了拷贝或移动操作时，该操作负责拷贝或移动包括基类部分成员在内的整个对象。

    // 定义派生类的拷贝或移动构造函数
    // 当为派生类定义拷贝或移动构造函数时（参见13.1.1节，第440页和13.6.2节，第473页），我们通常使用对应的基类构造函数初始化对象的基类部分：见D1。
    // 在默认情况下，基类默认构造函数初始化派生类对象的基类部分。如果我们想拷贝（或移动）基类部分，则必须在派生类的构造函数初始值列表中显式地使用基类的拷贝（或移动）构造函数。

    // 派生类赋值运算符
    // 与拷贝和移动构造函数一样，派生类的赋值运算符（参见13.1.2节，第443页和13.6.2节，第474页）也必须显式地为其基类部分赋值：见D1。

    // 派生类析构函数
    // 如前所述，在析构函数体执行完成后，对象的成员会被隐式销毁（参见13.1.3节，第445页）。类似的，对象的基类部分也是隐式销毁的。
    // 因此，和构造函数及赋值运算符不同的是，派生类析构函数只负责销毁由派生类自己分配的资源：见D1。
    // 对象销毁的顺序正好与其创建的顺序相反：派生类析构函数首先执行，然后是基类的析构函数，以此类推，沿着继承体系的反方向直至最后。

    // 在构造函数和析构函数中调用虚函数
    // 如我们所知，派生类对象的基类部分将首先被构建。当执行基类的构造函数时，该对象的派生类部分是未被初始化的状态。类似的，销毁派生类对象的次序正好相反，因此当执行基类的析构函数时，
    //   派生类部分已经被销毁掉了。由此可知，当我们执行上述基类成员的时候，该对象处于未完成的状态。
    // 如果构造函数或析构函数调用了某个虚函数，则我们应该执行与构造函数或析构函数所属类型相对应的虚函数版本。

    // 15.7.4 继承的构造函数
    // 一个类只初始化它的直接基类，出于同样的原因，一个类也只继承其直接基类的构造函数。类不能继承默认、拷贝和移动构造函数。如果派生类没有直接定义这些构造函数，则编译器将为派生类合成它们。
    // 派生类继承基类构造函数的方式是提供一条注明了（直接）基类名的using声明语句。举个例子，我们可以重新定义Bulk_quote类（参见15.4节，第541页），令其继承Disc_quote类的构造函数：
    // 通常情况下，using声明语句只是令某个名字在当前作用域内可见。而当作用于构造函数时，using声明语句将令编译器产生代码。对于基类的每个构造函数，编译器都生成一个与之对应的派生类构造函数。
    //   换句话说，对于基类的每个构造函数，编译器都在派生类中生成一个形参列表完全相同的构造函数。

    // 继承的构造函数的特点
    // 和普通成员的using声明不一样，一个构造函数的using声明不会改变该构造函数的访问级别。例如，不管using声明出现在哪儿，基类的私有构造函数在派生类中还是一个私有构造函数。
    // 而且，一个using声明语句不能指定explicit或constexpr。如果基类的构造函数是explicit（参见7.5.4节，第265页）或者constexpr（参见7.5.6节，第267页），则继承的构造函数也拥有相同的属性。
    // 当一个基类构造函数含有默认实参（参见6.5.1节，第211页）时，这些实参并不会被继承。相反，派生类将获得多个继承的构造函数，其中每个构造函数分别省略掉一个含有默认实参的形参。
    // 如果基类有一个接受两个形参的构造函数，其中第二个形参含有默认实参，则派生类将获得两个构造函数：
    //   一个构造函数接受两个形参（没有默认实参），另一个构造函数只接受一个形参，它对应于基类中最左侧的没有默认值的那个形参。
    // 默认、拷贝和移动构造函数不会被继承。这些构造函数按照正常规则被合成。继承的构造函数不会被作为用户定义的构造函数来使用，因此，如果一个类只含有继承的构造函数，则它也将拥有一个合成的默认构造函数。

    return 0;
}